Completed
Pull Request — master (#179)
by Olivier
02:35
created

SlideShow._getBackgroundColour   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 9.4285
1
/* global Gallery, Thumbnails, DOMPurify */
2
(function ($, OC, OCA, t) {
3
	"use strict";
4
	/**
5
	 * Slideshow featuring zooming
6
	 *
7
	 * @constructor
8
	 */
9
	var SlideShow = function () {
10
	};
11
12
	SlideShow.prototype = {
13
		slideshowTemplate: null,
14
		container: null,
15
		zoomablePreviewContainer: null,
16
		controls: null,
17
		imageCache: {},
18
		/** {Image} */
19
		currentImage: null,
20
		errorLoadingImage: false,
21
		onStop: null,
22
		zoomablePreview: null,
23
		active: false,
24
		backgroundToggle: false,
25
		// We need 6 hexas for comparison reasons
26
		darkBackgroundColour: '#000000',
27
		lightBackgroundColour: '#ffffff',
28
29
		/**
30
		 * Initialises the slideshow
31
		 *
32
		 * @param {boolean} autoPlay
33
		 * @param {number} interval
34
		 * @param {Array} features
35
		 */
36
		init: function (autoPlay, interval, features) {
37
			if (features.indexOf('background_colour_toggle') > -1) {
38
				this.backgroundToggle = true;
39
			}
40
41
			return $.when(this._getSlideshowTemplate()).then(function ($tmpl) {
42
				// Move the slideshow outside the content so we can hide the content
43
				$('body').append($tmpl);
44
				this.container = $('#slideshow');
45
				this.zoomablePreviewContainer = this.container.find('.bigshotContainer');
46
				this.zoomablePreview = new SlideShow.ZoomablePreview(this.container);
47
				this.controls =
48
					new SlideShow.Controls(
49
						this,
50
						this.container,
51
						this.zoomablePreview,
52
						interval,
53
						features);
54
				this.controls.init();
55
56
				this._initControlsAutoFader();
57
58
				// Only modern browsers can manipulate history
59
				if (history && history.pushState) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable history is declared in the current environment, consider using typeof history === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
60
					// Stop the slideshow when backing out.
61
					$(window).bind('popstate.slideshow', function () {
62
						if (this.active === true) {
63
							this.active = false;
64
							this.controls.stop();
65
						}
66
					}.bind(this));
67
				}
68
			}.bind(this)).fail(function () {
69
				OC.Notification.show(t('core', 'Error loading slideshow template'));
70
			});
71
		},
72
73
		/**
74
		 * Refreshes the slideshow's data
75
		 *
76
		 * @param {{name:string, url: string, path: string, fallBack: string}[]} images
77
		 * @param {boolean} autoPlay
78
		 */
79
		setImages: function (images, autoPlay) {
80
			this._hideImage();
81
			this.images = images;
82
			this.controls.update(images, autoPlay);
83
		},
84
85
		/**
86
		 * Launches the slideshow
87
		 *
88
		 * @param {number} index
89
		 *
90
		 * @returns {*}
91
		 */
92
		show: function (index) {
93
			this.hideErrorNotification();
94
			this.active = true;
95
			this.container.show();
96
			this.container.css('background-position', 'center');
97
			this._hideImage();
98
			this.container.find('.icon-loading-dark').show();
99
			var currentImageId = index;
100
			return this.loadImage(this.images[index]).then(function (img) {
101
				this.container.css('background-position', '-10000px 0');
102
103
				// check if we moved along while we were loading
104
				if (currentImageId === index) {
105
					var image = this.images[index];
106
					var transparent = this._isTransparent(image.mimeType);
107
					this.controls.showActionButtons(transparent, Gallery.token, image.permissions);
108
					this.errorLoadingImage = false;
109
					this.currentImage = img;
110
					img.setAttribute('alt', image.name);
111
					$(img).css('position', 'absolute');
112
					$(img).css('background-color', image.backgroundColour);
113
					if (transparent && this.backgroundToggle === true) {
114
						var $border = 30 / window.devicePixelRatio;
115
						$(img).css('outline', $border + 'px solid ' + image.backgroundColour);
116
					}
117
118
					this.zoomablePreview.startBigshot(img, this.currentImage, image.mimeType);
119
120
					this._setUrl(image.path);
121
					this.controls.show(currentImageId);
122
					this.container.find('.icon-loading-dark').hide();
123
				}
124
			}.bind(this), function () {
125
				// Don't do anything if the user has moved along while we were loading as it would
126
				// mess up the index
127
				if (currentImageId === index) {
128
					this.errorLoadingImage = true;
129
					this.showErrorNotification(null);
130
					this._setUrl(this.images[index].path);
131
					this.images.splice(index, 1);
132
					this.controls.updateControls(this.images, this.errorLoadingImage);
133
				}
134
			}.bind(this));
135
		},
136
137
		/**
138
		 * Loads the image to show in the slideshow and preloads the next one
139
		 *
140
		 * @param {Object} preview
141
		 *
142
		 * @returns {*}
143
		 */
144
		loadImage: function (preview) {
145
			var url = preview.url;
146
			var mimeType = preview.mimeType;
147
148
			if (!this.imageCache[url]) {
149
				this.imageCache[url] = new $.Deferred();
150
				var image = new Image();
0 ignored issues
show
Bug introduced by
The variable Image seems to be never declared. If this is a global, consider adding a /** global: Image */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
151
152
				image.onload = function () {
153
					preview.backgroundColour = this._getBackgroundColour(image);
154
					if (this.imageCache[url]) {
155
						this.imageCache[url].resolve(image);
156
					}
157
				}.bind(this);
158
				image.onerror = function () {
159
					if (this.imageCache[url]) {
160
						this.imageCache[url].reject(url);
161
					}
162
				}.bind(this);
163
				if (mimeType === 'image/svg+xml') {
164
					image.src = this._getSVG(url);
165
				} else {
166
					image.src = url;
167
				}
168
			}
169
			return this.imageCache[url];
170
		},
171
172
		/**
173
		 * Shows a new image in the slideshow and preloads the next in the list
174
		 *
175
		 * @param {number} current
176
		 * @param {Object} next
177
		 */
178
		next: function (current, next) {
179
			this.show(current).then(function () {
180
				// Preloads the next image in the list
181
				this.loadImage(next);
182
			}.bind(this));
183
		},
184
185
		/**
186
		 * Determines which colour to use for the background
187
		 *
188
		 * @param {*} image
189
		 *
190
		 * @returns {string}
191
		 * @private
192
		 */
193
		_getBackgroundColour: function (image) {
194
			var backgroundColour = this.darkBackgroundColour;
195
			if (this._isTransparent(image.mimeType) && this._isMainlyDark(image)) {
196
				backgroundColour = this.lightBackgroundColour;
197
			}
198
			return backgroundColour;
199
		},
200
201
		/**
202
		 * Calculates the luminance of an image to determine if an image is mainly dark
203
		 *
204
		 * @param {*} image
205
		 *
206
		 * @returns {boolean}
207
		 * @private
208
		 */
209
		_isMainlyDark: function (image) {
210
			var isMainlyDark = true;
211
			var numberOfSamples = 1000; // Seems to be the sweet spot
212
			// The name has to be 'canvas'
213
			var lumiCanvas = document.createElement('canvas');
214
215
			var imgArea = image.width * image.height;
216
			var canArea = numberOfSamples;
217
			var factor = Math.sqrt(canArea / imgArea);
218
219
			var scaledWidth = factor * image.width;
220
			var scaledHeight = factor * image.height;
221
			lumiCanvas.width = scaledWidth;
222
			lumiCanvas.height = scaledHeight;
223
			var lumiCtx = lumiCanvas.getContext('2d');
224
			lumiCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
225
			var imgData = lumiCtx.getImageData(0, 0, lumiCanvas.width, lumiCanvas.height);
226
			var pix = imgData.data; // pix.length will be approximately 4*numberOfSamples (for RGBA)
227
			var pixelArraySize = pix.length;
228
			var totalLuminance = 0;
229
			var sampleNumber = 1;
230
			var averageLuminance;
231
			var totalAlpha = 0;
232
			var alphaLevel;
233
			var red = 0;
234
			var green = 0;
235
			var blue = 0;
236
			var alpha = 0;
237
			var lum = 0;
238
			var alphaThreshold = 0.1;
239
240
			var sampleCounter = 0;
241
			var itemsPerPixel = 4; // red, green, blue, alpha
242
			var sampleSize = Math.floor(pixelArraySize / (itemsPerPixel * numberOfSamples));
243
			if (sampleSize <= 0) {
244
				sampleSize = 1;
245
			}
246
			// i += 4 because 4 colours for every pixel
247
			for (var i = 0, n = pixelArraySize; i < n; i += itemsPerPixel * sampleSize) {
248
				sampleCounter++;
249
				alpha = pix[i + 3] / 255;
250
				totalAlpha += alpha;
251
				if (Math.ceil(alpha * 100) / 100 > alphaThreshold) {
252
					red = pix[i];
253
					green = pix[i + 1];
254
					blue = pix[i + 2];
255
					// Luminance formula from
256
					// http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
257
					lum = (red + red + green + green + green + blue) / 6;
258
					//lum = (red * 0.299 + green * 0.587 + blue * 0.114 );
259
					totalLuminance += lum * alpha;
260
					sampleNumber++;
261
				}
262
			}
263
264
			// Deletes the canvas
265
			lumiCanvas = null;
0 ignored issues
show
Unused Code introduced by
The assignment to lumiCanvas seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
266
267
			// Calculate the optimum background colour for this image
268
			averageLuminance = Math.ceil((totalLuminance / sampleNumber) * 100) / 100;
269
			alphaLevel = Math.ceil((totalAlpha / numberOfSamples) * 100);
270
			if (averageLuminance < 50 && alphaLevel < 90) {
271
				isMainlyDark = false;
272
			}
273
274
			return isMainlyDark;
275
		},
276
277
		/**
278
		 * Stops the slideshow
279
		 */
280
		stop: function () {
281
			this.active = false;
282
			this.images = null;
283
			this._hideImage();
284
			if (this.onStop) {
285
				this.onStop();
286
			}
287
		},
288
289
		/**
290
		 * Sends the current image as a download
291
		 *
292
		 * @param {string} downloadUrl
293
		 *
294
		 * @returns {boolean}
295
		 */
296
		getImageDownload: function (downloadUrl) {
297
			OC.redirect(downloadUrl);
298
			return false;
299
		},
300
301
		/**
302
		 * Changes the colour of the background of the image
303
		 */
304
		toggleBackground: function () {
305
			var toHex = function (x) {
306
				return ("0" + parseInt(x).toString(16)).slice(-2);
307
			};
308
			var container = this.zoomablePreviewContainer.children('img');
309
			var rgb = container.css('background-color').match(/\d+/g);
310
			var hex = "#" + toHex(rgb[0]) + toHex(rgb[1]) + toHex(rgb[2]);
311
			var $border = 30 / window.devicePixelRatio;
312
			var newBackgroundColor;
313
314
			// Grey #363636
315
			if (hex === this.darkBackgroundColour) {
316
				newBackgroundColor = this.lightBackgroundColour;
317
			} else {
318
				newBackgroundColor = this.darkBackgroundColour;
319
			}
320
321
			container.css('background-color', newBackgroundColor);
322
			if (this.backgroundToggle === true) {
323
				container.css('outline', $border + 'px solid ' + newBackgroundColor);
324
			}
325
326
		},
327
328
		/**
329
		 * Shows an error notification
330
		 *
331
		 * @param {string} message
332
		 */
333
		showErrorNotification: function (message) {
334
			if ($.isEmptyObject(message)) {
335
				message = t('gallery',
336
					'<strong>Error!</strong> Could not generate a preview of this file.<br>' +
337
					'Please go to the next slide while we remove this image from the slideshow');
338
			}
339
			this.container.find('.notification').html(message);
340
			this.container.find('.notification').show();
341
			this.controls.hideButton('.changeBackground');
342
		},
343
344
		/**
345
		 * Hides the error notification
346
		 */
347
		hideErrorNotification: function () {
348
			this.container.find('.notification').hide();
349
			this.container.find('.notification').html('');
350
		},
351
352
		/**
353
		 * Removes a specific button from the interface
354
		 *
355
		 * @param button
356
		 */
357
		removeButton: function (button) {
358
			this.controls.removeButton(button);
359
		},
360
361
		/**
362
		 * Deletes an image from the slideshow
363
		 *
364
		 * @param {object} image
365
		 * @param {number} currentIndex
366
		 */
367
		deleteImage: function (image, currentIndex) {
368
			// These are Gallery specific commands to be replaced
369
			// which should sit somewhere else
370
			if (!window.galleryFileAction) {
371
				delete Gallery.imageMap[image.path];
372
				delete Thumbnails.map[image.file];
373
				Gallery.albumMap[Gallery.currentAlbum].images.splice(currentIndex, 1);
374
				Gallery.view.init(Gallery.currentAlbum);
375
			}
376
		},
377
378
		/**
379
		 * Automatically fades the controls after 3 seconds
380
		 *
381
		 * @private
382
		 */
383
		_initControlsAutoFader: function () {
384
			var inactiveCallback = function () {
385
				this.container.addClass('inactive');
386
			}.bind(this);
387
			var inactiveTimeout = setTimeout(inactiveCallback, 3000);
388
389
			this.container.on('mousemove touchstart', function () {
390
				this.container.removeClass('inactive');
391
				clearTimeout(inactiveTimeout);
392
				inactiveTimeout = setTimeout(inactiveCallback, 3000);
393
			}.bind(this));
394
		},
395
396
		/**
397
		 * Simplest way to detect if image is transparent.
398
		 *
399
		 * That's very inaccurate since it doesn't include images which support transparency
400
		 *
401
		 * @param mimeType
402
		 * @returns {boolean}
403
		 * @private
404
		 */
405
		_isTransparent: function (mimeType) {
406
			return !(mimeType === 'image/jpeg'
407
				|| mimeType === 'image/x-dcraw'
408
				|| mimeType === 'application/font-sfnt'
409
				|| mimeType === 'application/x-font'
410
			);
411
		},
412
413
		/**
414
		 * Changes the browser Url, based on the current image
415
		 *
416
		 * @param {string} path
417
		 * @private
418
		 */
419
		_setUrl: function (path) {
420
			if (history && history.replaceState) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable history is declared in the current environment, consider using typeof history === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
421
				history.replaceState('', '', '#' + encodeURI(path));
422
			}
423
		},
424
425
		/**
426
		 * Hides the current image (before loading the next)
427
		 *
428
		 * @private
429
		 */
430
		_hideImage: function () {
431
			this.zoomablePreviewContainer.empty();
432
			this.controls.hideActionButtons();
433
		},
434
435
		/**
436
		 * Retrieves an SVG
437
		 *
438
		 * An SVG can't be simply attached to a src attribute like a bitmap image
439
		 *
440
		 * @param {string} source
441
		 *
442
		 * @returns {*}
443
		 * @private
444
		 */
445
		_getSVG: function (source) {
446
			var svgPreview = null;
447
			// DOMPurify only works with IE10+ and we load SVGs in the IMG tag
448
			if (window.btoa &&
449
				document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image",
450
					"1.1")) {
451
				var xmlHttp = new XMLHttpRequest();
0 ignored issues
show
Bug introduced by
The variable XMLHttpRequest seems to be never declared. If this is a global, consider adding a /** global: XMLHttpRequest */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
452
				xmlHttp.open("GET", source, false);
453
				xmlHttp.send(null);
454
				if (xmlHttp.status === 200) {
455
					var pureSvg = DOMPurify.sanitize(xmlHttp.responseText, {ADD_TAGS: ['filter']});
456
					// Remove XML comment garbage left in the purified data
457
					var badTag = pureSvg.indexOf(']&gt;');
458
					var fixedPureSvg = pureSvg.substring(badTag < 0 ? 0 : 5, pureSvg.length);
459
					svgPreview = "data:image/svg+xml;base64," + window.btoa(fixedPureSvg);
460
				}
461
			}
462
463
			return svgPreview;
464
		},
465
466
		/**
467
		 * Retrieves the slideshow's template
468
		 *
469
		 * @returns {*}
470
		 * @private
471
		 */
472
		_getSlideshowTemplate: function () {
473
			var defer = $.Deferred();
474
			if (!this.$slideshowTemplate) {
475
				var self = this;
476
				var url = OC.generateUrl('apps/gallery/slideshow', null);
477
				$.get(url, function (tmpl) {
478
					var template = $(tmpl);
479
					var tmplButton;
480
					var buttonsArray = [
481
						{
482
							el: '.next',
483
							trans: t('gallery', 'Next')
484
						},
485
						{
486
							el: '.play',
487
							trans: t('gallery', 'Play')
488
						},
489
						{
490
							el: '.pause',
491
							trans: t('gallery', 'Pause')
492
						},
493
						{
494
							el: '.previous',
495
							trans: t('gallery', 'Previous')
496
						},
497
						{
498
							el: '.exit',
499
							trans: t('gallery', 'Close')
500
						},
501
						{
502
							el: '.downloadImage',
503
							trans: t('gallery', 'Download'),
504
							toolTip: true
505
						},
506
						{
507
							el: '.changeBackground',
508
							trans: t('gallery', 'Toggle background'),
509
							toolTip: true
510
						},
511
						{
512
							el: '.deleteImage',
513
							trans: t('gallery', 'Delete'),
514
							toolTip: true
515
						}
516
					];
517
					for (var i = 0; i < buttonsArray.length; i++) {
518
						var button = buttonsArray[i];
519
520
						tmplButton = template.find(button.el);
521
						tmplButton.val(button.trans);
522
						if (button.toolTip) {
523
							tmplButton.attr("title", button.trans);
524
						}
525
					}
526
					self.$slideshowTemplate = template;
527
					defer.resolve(self.$slideshowTemplate);
528
				})
529
					.fail(function () {
530
						defer.reject();
531
					});
532
			} else {
533
				defer.resolve(this.$slideshowTemplate);
534
			}
535
			return defer.promise();
536
		}
537
	};
538
539
	window.SlideShow = SlideShow;
540
})(jQuery, OC, OCA, t);
541